/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.bmi.gwt.mi.rebind;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.bmi.gwt.mi.client.impl.ProxyFactoryClient.InnerProxyFactory;
import org.bmi.gwt.mi.shared.MiProxy;
import org.bmi.gwt.mi.shared.annotations.Protected;
import org.bmi.gwt.mi.shared.annotations.TargetMiObjectClass;
import org.bmi.gwt.mi.shared.exceptions.NotAvailableConstructorException;
import org.bmi.gwt.mi.shared.util.ProxyBase;

import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

class ProxyFactoryCreator
{
	/** Simple name of class to be generated */
	private String generatedClassName = null;

	/** Package name of class to be generated */
	private String packageName = null;

	/** The type of provided interface */
	private JClassType providedType;

	private ArrayList<ProxyCreator> proxyCreators;

	/**
	 * Interface containing information required to generate implementations of
	 * proxies interfaces.
	 */
	interface ProxyCreatorHelper
	{
		JClassType getProxyType();

		JClassType getTargteClass();

		List<JClassType> getProxiesToClass(JClassType targetClass);
	}

	ProxyFactoryCreator()
	{}

	/**
	 * Creates a factory class providing {@link MiProxy} user defined classes.
	 * 
	 * @param logger
	 * @param context
	 * @param typeName
	 * @return
	 * @throws UnableToCompleteException
	 */
	String create(TreeLogger logger, GeneratorContext context, String typeName) throws UnableToCompleteException
	{
		this.proxyCreators = new ArrayList<ProxyCreator>();

		TypeOracle typeOracle = context.getTypeOracle();

		try
		{
			this.providedType = typeOracle.getType(typeName);
		}
		catch (NotFoundException e)
		{
			logger.log(TreeLogger.ERROR, "Provided class '" + typeName + "' not found", e);
			throw new UnableToCompleteException();
		}

		this.generatedClassName = this.providedType.getSimpleSourceName() + "___Impl";
		this.packageName = this.providedType.getPackage().getName();

		String canonicalName = this.packageName + "." + this.generatedClassName;

		ArrayList<ProxyCreatorHelper> foundProxies = new ArrayList<ProxyCreatorHelper>(20);
		final HashMap<String, List<JClassType>> proxiesMap = new HashMap<String, List<JClassType>>();

		try
		{
			JClassType proxyType = typeOracle.getType(MiProxy.class.getCanonicalName());

			for (final JClassType t : proxyType.getSubtypes())
				/*
				 * enabling only those who hasn't the protected annotation, and
				 * are not the base interface.
				 */
				if (!t.getQualifiedSourceName().equals(proxyType.getQualifiedSourceName())
						&& t.getAnnotation(Protected.class) == null)
				{
					TargetMiObjectClass anno = t.getAnnotation(TargetMiObjectClass.class);

					if (anno == null)
					{
						logger.log(TreeLogger.ERROR, "Class " + t.getQualifiedSourceName()
								+ " should be annotated with " + TargetMiObjectClass.class.getName());

						throw new UnableToCompleteException();
					}

					final JClassType targetClass = typeOracle.findType(anno.miObjectClass().getCanonicalName());

					String targetName = targetClass.getQualifiedSourceName();

					if (proxiesMap.containsKey(targetName))
						proxiesMap.get(targetName).add(proxyType);
					else
					{
						ArrayList<JClassType> l = new ArrayList<JClassType>(5);
						l.add(proxyType);

						proxiesMap.put(targetName, l);
					}

					foundProxies.add(new ProxyCreatorHelper()
					{
						@Override
						public JClassType getTargteClass()
						{
							return targetClass;
						}

						@Override
						public JClassType getProxyType()
						{
							return t;
						}

						@Override
						public List<JClassType> getProxiesToClass(JClassType targetClass)
						{
							return proxiesMap.get(targetClass.getQualifiedSourceName());
						}
					});
				}

		}
		catch (NotFoundException e)
		{
			logger.log(TreeLogger.ERROR, "Can't find subtypes of " + MiProxy.class.getCanonicalName(), e);
			throw new UnableToCompleteException();
		}

		for (ProxyCreatorHelper helper : foundProxies)
			this.proxyCreators.add(new ProxyCreator(logger, context, helper));

		generateClass(logger, context, typeOracle);

		logger.log(TreeLogger.INFO, "Generated class " + canonicalName);

		return canonicalName;
	}

	private void generateClass(TreeLogger logger, GeneratorContext context, TypeOracle typeOracle)
			throws UnableToCompleteException
	{
		PrintWriter printWriter = context.tryCreate(logger, this.packageName, this.generatedClassName);

		/*
		 * When the print writer is null, the source code has ALREADY been
		 * generated, in such case nothing will be done.
		 */
		if (printWriter == null) return;

		/* initialize composer, set class properties, create source writer */
		ClassSourceFileComposerFactory composer = null;
		composer = new ClassSourceFileComposerFactory(this.packageName, this.generatedClassName);

		composer.setSuperclass(this.providedType.getQualifiedSourceName());

		SourceWriter sourceWriter = null;
		sourceWriter = composer.createSourceWriter(context, printWriter);

		/* Generating constructor and required methods */
		generateConstructor(logger, null, sourceWriter, typeOracle);

		/* close generated class */
		sourceWriter.outdent();
		sourceWriter.println("}");

		context.commit(logger, printWriter);
	}

	/**
	 * Generate source code for the default constructor.
	 * 
	 * @param logger
	 * @param context The generator context
	 * @param w Source writer to output source code.
	 * @param typeOracle
	 * @throws UnableToCompleteException
	 * @throws NotFoundException
	 */
	private void generateConstructor(TreeLogger logger, GeneratorContext context, SourceWriter w, TypeOracle typeOracle)
			throws UnableToCompleteException
	{
		w.println("public " + this.generatedClassName + "() { ");
		w.indent();

		w.println("super();");

		for (ProxyCreator prxCreator : this.proxyCreators)
			addInvoker(logger, context, w, prxCreator);

		w.outdent();
		w.println("}");
	}

	private void addInvoker(TreeLogger logger, GeneratorContext context, SourceWriter w, ProxyCreator prxCreator)
			throws UnableToCompleteException
	{
		w.println();

		logger.log(TreeLogger.INFO, "Adding to the factory " + prxCreator.getInterfaceType().getQualifiedSourceName());

		w.print("super.proxies.put(%s.class.getName(), ", prxCreator.getInterfaceType().getQualifiedSourceName());
		w.println("new %s(){", InnerProxyFactory.class.getCanonicalName());
		w.indent();

		w.println("public %s create() {", ProxyBase.class.getCanonicalName());
		w.indent();

		w.print("return new %s();", prxCreator.getCanonicalName());

		w.outdent();
		w.println("}");

		w.println("public %s create(Object[] constructorArgs) throws %s {", ProxyBase.class.getCanonicalName(),
				NotAvailableConstructorException.class.getCanonicalName());
		w.indent();

		if (prxCreator.hasConstructor())
			w.print("return new %s(constructorArgs);", prxCreator.getCanonicalName());
		else
			w.print("throw new %s();", NotAvailableConstructorException.class.getCanonicalName());
		w.outdent();

		w.println("}");

		w.outdent();
		w.println("});");
	}
}
